#region Namespaces

using System;
using System.IO;
using System.Xml;

#endregion

namespace Vibstudio.X4NET.Xml.Linq
{
    public class XDocument : XContainer
    {
        public XDocument()
        {
        }

        public XDocument(params object[] content)
        {
            Add(content);
        }

        public XDocument(XDeclaration declaration, params object[] content)
        {
            Declaration = declaration;
            Add(content);
        }

        public XDocument(XDocument other)
        {
            foreach (XNode o in other.Nodes())
            {
                Add(XUtil.Clone(o));
            }
        }

        public XDeclaration Declaration { get; set; }

        public XDocumentType DocumentType
        {
            get
            {
                foreach (XNode o in Nodes())
                {
                    if (o is XDocumentType)
                    {
                        return (XDocumentType)o;
                    }
                }
                return null;
            }
        }

        public override XmlNodeType NodeType
        {
            get { return XmlNodeType.Document; }
        }

        public XElement Root
        {
            get
            {
                foreach (XNode o in Nodes())
                {
                    if (o is XElement)
                    {
                        return (XElement)o;
                    }
                }
                return null;
            }
        }

        public static XDocument Load(string uri)
        {
            return Load(uri, LoadOptions.None);
        }

        public static XDocument Load(string uri, LoadOptions options)
        {
            XmlReaderSettings s = new XmlReaderSettings();
            s.ProhibitDtd = false;
            s.IgnoreWhitespace = (options & LoadOptions.PreserveWhitespace) == 0;
            using (XmlReader r = XmlReader.Create(uri, s))
            {
                return LoadCore(r, options);
            }
        }

        public static XDocument Load(Stream stream)
        {
            return Load(new StreamReader(stream), LoadOptions.None);
        }

        public static XDocument Load(Stream stream, LoadOptions options)
        {
            return Load(new StreamReader(stream), options);
        }

        public static XDocument Load(TextReader textReader)
        {
            return Load(textReader, LoadOptions.None);
        }

        public static XDocument Load(TextReader textReader, LoadOptions options)
        {
            XmlReaderSettings s = new XmlReaderSettings();
            s.ProhibitDtd = false;
            s.IgnoreWhitespace = (options & LoadOptions.PreserveWhitespace) == 0;
            using (XmlReader r = XmlReader.Create(textReader, s))
            {
                return LoadCore(r, options);
            }
        }

        public static XDocument Load(XmlReader reader)
        {
            return Load(reader, LoadOptions.None);
        }

        public static XDocument Load(XmlReader reader, LoadOptions options)
        {
            XmlReaderSettings s = reader.Settings != null ? reader.Settings.Clone() : new XmlReaderSettings();
            s.IgnoreWhitespace = (options & LoadOptions.PreserveWhitespace) == 0;
            using (XmlReader r = XmlReader.Create(reader, s))
            {
                return LoadCore(r, options);
            }
        }

        private static XDocument LoadCore(XmlReader reader, LoadOptions options)
        {
            XDocument doc = new XDocument();
            doc.ReadContent(reader, options);
            return doc;
        }

        private void ReadContent(XmlReader reader, LoadOptions options)
        {
            if (reader.ReadState == ReadState.Initial)
            {
                reader.Read();
            }
            FillLineInfoAndBaseUri(reader, options);
            if (reader.NodeType == XmlNodeType.XmlDeclaration)
            {
                Declaration = new XDeclaration(
                    reader.GetAttribute("version"),
                    reader.GetAttribute("encoding"),
                    reader.GetAttribute("standalone"));
                reader.Read();
            }
            ReadContentFrom(reader, options);
            if (Root == null)
            {
                throw new InvalidOperationException("The document element is missing.");
            }
        }

        private static void ValidateWhitespace(string s)
        {
            for (int i = 0; i < s.Length; i++)
            {
                switch (s[i])
                {
                    case ' ':
                    case '\t':
                    case '\n':
                    case '\r':
                        continue;
                    default:
                        throw new ArgumentException("Non-whitespace text appears directly in the document.");
                }
            }
        }

        public static XDocument Parse(string text)
        {
            return Parse(text, LoadOptions.None);
        }

        public static XDocument Parse(string text, LoadOptions options)
        {
            return Load(new StringReader(text), options);
        }

        public void Save(string fileName)
        {
            Save(fileName, SaveOptions.None);
        }

        public void Save(string fileName, SaveOptions options)
        {
            XmlWriterSettings s = new XmlWriterSettings();
            if ((options & SaveOptions.DisableFormatting) == SaveOptions.None)
            {
                s.Indent = true;
            }

            using (XmlWriter w = XmlWriter.Create(fileName, s))
            {
                Save(w);
            }
        }

        public void Save(TextWriter textWriter)
        {
            Save(textWriter, SaveOptions.None);
        }

        public void Save(TextWriter textWriter, SaveOptions options)
        {
            XmlWriterSettings s = new XmlWriterSettings();
            if ((options & SaveOptions.DisableFormatting) == SaveOptions.None)
            {
                s.Indent = true;
            }

            using (XmlWriter w = XmlWriter.Create(textWriter, s))
            {
                Save(w);
            }
        }

        public void Save(XmlWriter writer)
        {
            WriteTo(writer);
        }

        public override void WriteTo(XmlWriter writer)
        {
            if (Declaration != null && Declaration.Standalone != null)
            {
                writer.WriteStartDocument(Declaration.Standalone == "yes");
            }
            else
            {
                writer.WriteStartDocument();
            }
            foreach (XNode node in Nodes())
            {
                node.WriteTo(writer);
            }
        }

        internal override bool OnAddingObject(object obj, bool rejectAttribute, XNode refNode, bool addFirst)
        {
            VerifyAddedNode(obj, addFirst);
            return false;
        }

        private void VerifyAddedNode(object node, bool addFirst)
        {
            if (node == null)
            {
                throw new InvalidOperationException("Only a node is allowed here");
            }

            if (node is string)
            {
                ValidateWhitespace((string)node);
            }
            if (node is XText)
            {
                ValidateWhitespace(((XText)node).Value);
            }
            else if (node is XDocumentType)
            {
                if (DocumentType != null)
                {
                    throw new InvalidOperationException("There already is another document type declaration");
                }
                if (Root != null && !addFirst)
                {
                    throw new InvalidOperationException("A document type cannot be added after the document element");
                }
            }
            else if (node is XElement)
            {
                if (Root != null)
                {
                    throw new InvalidOperationException("There already is another document element");
                }
                if (DocumentType != null && addFirst)
                {
                    throw new InvalidOperationException(
                        "An element cannot be added before the document type declaration");
                }
            }
        }
    }
}